import { Buffer } from 'node:buffer';
import { Webhook } from 'standardwebhooks';
import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest';
import { createInvalidSignatureFormatError, createUnsupportedSignatureVersionError } from './handler/handler.errors';
import { arrayBufferToBase64, base64ToArrayBuffer, signBody, verifySignature } from './signature';

const arrayBuffer = (str: string) => new TextEncoder().encode(str).buffer as ArrayBuffer;

describe('signature', () => {
  describe('signBody', () => {
    test('a buffer can be signed with a secret, the resulting signature is a base64 encoded string', async () => {
      const payload = { event: 'foo.bar', payload: { biz: 'baz' }, now: new Date('2025-07-25') };
      const serializedPayload = JSON.stringify(payload);
      const webhookId = 'msg_a1hm8ojetdjhf5boqd2hz244';
      const timestamp = '1753390766';
      const secret = 'secret-key';

      const { signature } = await signBody({ serializedPayload, webhookId, timestamp, secret });

      expect(signature).to.equal('v1,POSJo83MmyWmTh3NJOtEpBZSn+CmdpjHSS05p3wYAVE=');
    });
  });

  describe('verifySignature', () => {
    test('verify that the signature of a buffer has been created with a given secret', async () => {
      const payload = { event: 'foo.bar', payload: { biz: 'baz' }, now: new Date('2025-07-25') };
      const serializedPayload = JSON.stringify(payload);
      const webhookId = 'msg_a1hm8ojetdjhf5boqd2hz244';
      const timestamp = '1753390766';
      const secret = 'secret-key';
      const signature = 'v1,POSJo83MmyWmTh3NJOtEpBZSn+CmdpjHSS05p3wYAVE=';

      const result = await verifySignature({ serializedPayload, webhookId, timestamp, signature, secret });

      expect(result).to.equal(true);
    });

    test('an error is thrown when the version is not supported', async () => {
      const payload = { event: 'foo.bar', payload: { biz: 'baz' }, now: new Date('2025-07-25') };
      const serializedPayload = JSON.stringify(payload);
      const webhookId = 'msg_a1hm8ojetdjhf5boqd2hz244';
      const timestamp = '1753390766';
      const secret = 'secret-key';
      const signature = 'v2,POSJo83MmyWmTh3NJOtEpBZSn+CmdpjHSS05p3wYAVE=';

      expect(verifySignature({ serializedPayload, webhookId, timestamp, signature, secret })).rejects.toThrow(createUnsupportedSignatureVersionError());
    });

    test('an error is thrown when the signature is not valid', async () => {
      const payload = { event: 'foo.bar', payload: { biz: 'baz' }, now: new Date('2025-07-25') };
      const serializedPayload = JSON.stringify(payload);
      const webhookId = 'msg_a1hm8ojetdjhf5boqd2hz244';
      const timestamp = '1753390766';
      const secret = 'secret-key';
      const signature = '';

      expect(verifySignature({ serializedPayload, webhookId, timestamp, signature, secret })).rejects.toThrow(createInvalidSignatureFormatError());
    });
  });

  describe('standardwebhooks compatibility', () => {
    // Because standardwebhooks uses hardcoded Date.now()
    beforeEach(() => {
      vi.useFakeTimers();
    });

    afterEach(() => {
      vi.useRealTimers();
    });

    test('a signed payload can be verified using the "standardwebhooks" package', async () => {
      const payload = { event: 'foo.bar', payload: { biz: 'baz' }, now: new Date('2025-07-25') };
      const serializedPayload = JSON.stringify(payload);
      const webhookId = 'msg_a1hm8ojetdjhf5boqd2hz244';
      const timestamp = '1753390766';
      const secret = 'secret-key';

      // Because standardwebhooks uses hardcoded Date.now() to check for webhook expiration...
      vi.setSystemTime(new Date(Number(timestamp) * 1000));

      const webhook = new Webhook(Buffer.from(secret).toString('base64'));

      const result = await webhook.verify(serializedPayload, {
        'webhook-id': webhookId,
        'webhook-timestamp': timestamp,
        'webhook-signature': 'v1,POSJo83MmyWmTh3NJOtEpBZSn+CmdpjHSS05p3wYAVE=',
      });

      expect(result).to.eql({
        event: 'foo.bar',
        payload: { biz: 'baz' },
        now: '2025-07-25T00:00:00.000Z',
      });
    });

    test('the signature is the same as the one generated by the "standardwebhooks" package', async () => {
      const payload = { event: 'foo.bar', payload: { biz: 'baz' }, now: new Date('2025-07-25') };
      const serializedPayload = JSON.stringify(payload);
      const webhookId = 'msg_a1hm8ojetdjhf5boqd2hz244';
      const timestamp = '1753390766';
      const secret = 'secret-key';

      const { signature } = await signBody({ serializedPayload, webhookId, timestamp, secret });

      const standardWebhookSignature = new Webhook(Buffer.from(secret).toString('base64')).sign(webhookId, new Date(Number(timestamp) * 1000), serializedPayload);

      expect(standardWebhookSignature).to.equal(signature);
    });
  });

  describe('arrayBufferToBase64', () => {
    test('a buffer can be converted to a base64 encoded string', () => {
      expect(arrayBufferToBase64(arrayBuffer('test'))).to.equal('dGVzdA==');
      expect(arrayBufferToBase64(arrayBuffer(''))).to.equal('');
    });
  });

  describe('base64ToArrayBuffer', () => {
    test('a base64 encoded string can be converted to a buffer', () => {
      expect(base64ToArrayBuffer('dGVzdA==')).to.deep.equal(arrayBuffer('test'));
      expect(base64ToArrayBuffer('')).to.deep.equal(arrayBuffer(''));
    });

    test('an error is thrown when the base64 encoded string is invalid', () => {
      expect(() => base64ToArrayBuffer('invalid--')).to.throw('Invalid character');
    });

    test('a buffer can be converted to a base64 encoded string and back to a buffer', () => {
      expect(
        base64ToArrayBuffer(
          arrayBufferToBase64(
            arrayBuffer('test'),
          ),
        ),
      ).to.deep.equal(arrayBuffer('test'));
    });
  });
});
